package gitee.derumin.railway_accelerator.things;

import gitee.derumin.lib_for_derumin.helpers.calculators.MathTools;
import gitee.derumin.lib_for_derumin.helpers.calculators.NotSoPrecisedPlaneCalculator;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.vehicle.AbstractMinecartEntity;
import net.minecraft.nbt.*;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.random.Random;

import java.util.*;

import static gitee.derumin.railway_accelerator.Railway_accelerator.CONFIG;

public class MinecartTrain {
    public static final double STANDARD_MASS = 2*5d + 1*(1d+1d) +1*(1d+5.4d);
    private static final Set<MinecartTrain> ALL_TRAINS = new HashSet<>();
    private static final int MaxLength = 8;
    private static final String NBT_KEY = "belong_to_train";

    private final ServerWorld world;
    private List<MinecartUnit> units;
    private final Set<Entity> crashedEntities;
    private double tickRemains;

    public static MinecartTrain loadFromNbt(NbtCompound compound, AbstractMinecartEntity entity){
        if (entity.getWorld() instanceof ServerWorld serverWorld){
            try {
                for (MinecartTrain train : ALL_TRAINS){
                    if (train.tryAccept(entity)){
                        return train;
                    }
                }
                MinecartTrain train = new MinecartTrain(serverWorld, compound.getList(NBT_KEY, NbtElement.INT_ARRAY_TYPE));
                if (ALL_TRAINS.add(train) && train.tryAccept(entity)){
                    return train;
                }else {
                    train.remove();
                }
            }catch (Exception ignored){}
        }
        return null;
    }

    public static void writeToNbt(NbtCompound compound, MinecartTrain train){
        if (train == null){
            return;
        }
        NbtList list = new NbtList();
        for (MinecartUnit unit : train.units){
            list.add(unit.toNbt());
        }
        compound.put(NBT_KEY, list);
    }

    public static boolean link(AbstractMinecartEntity existingEntity, AbstractMinecartEntity newEntity){
        if (existingEntity.getWorld().isClient() || existingEntity.isRemoved()){
            return false;
        }
        MinecartTrain train = existingEntity.railway_accelerator$getTrain();
        if (train == null){
            train = new MinecartTrain(existingEntity);
            ALL_TRAINS.add(train);
        }
        if (train.units.size() >= MaxLength || train.notReady()){
            return false;
        }
        double d = newEntity.railway_accelerator$getHorizontalVector()
                .innerProduct(existingEntity.railway_accelerator$getHorizontalVector());
        if (d < 0d && train.isHead(existingEntity)) {
            if (existingEntity.railway_accelerator$getLineVelocity() < 1.0E-3){
                train.flip();
                train.units.addLast(train.new MinecartUnit(newEntity));
            }else {
                newEntity.railway_accelerator$flip();
                train.units.addFirst(train.new MinecartUnit(newEntity));
            }
            return true;
        }
        if (d > 0d && train.isTail(existingEntity)) {
            train.units.addLast(train.new MinecartUnit(newEntity));
            return true;
        }
        return false;
    }

    public static void onTickStart(){
        List<MinecartTrain> disabledTrains = new ArrayList<>();
        for (MinecartTrain train : ALL_TRAINS){
            if (train.startTick()){
                disabledTrains.add(train);
            }
        }
        disabledTrains.forEach(MinecartTrain::remove);
    }

    public void crashOnEntity(Entity victim, AbstractMinecartEntity train){
        if (victim == null
                || train.hasPassenger(victim)
                || isNeighbor(train, victim)
                || this.crashedEntities.contains(victim)){
            return;
        }
        if (victim.hasPassengers()){
            crashOnEntity(victim.getFirstPassenger(), train);
        }
        double vx = train.getVelocity().x -victim.getVelocity().x;
        double vz = train.getVelocity().z -victim.getVelocity().z;
        double v = vx*vx +vz*vz;
        if (v < 1.0E-7){
            return;
        }
        v = Math.sqrt(v);
        double m = getMass();
        if (victim instanceof AbstractMinecartEntity minecart) {
            minecart.railway_accelerator$forceNotOnRail();
            if (minecart.railway_accelerator$getTrain() != null) {
                minecart.railway_accelerator$getTrain().onCollision(minecart);
            }
        }
        if (victim.isPushable()) {
            double multiplier = 2d * m / (1d + m);
            Random random = train.getRandom();
            double r = random.nextDouble();
            double vx1;
            double vz1;
            try {
                NotSoPrecisedPlaneCalculator.Dot dot = new NotSoPrecisedPlaneCalculator.Dot(victim.getX() - train.getX(), victim.getZ() - train.getZ());
                double d = NotSoPrecisedPlaneCalculator.Line.from(0, 0, train.getVelocity().x, train.getVelocity().z).distanceTo(dot);
                if (d > 0.01d) {
                    if (vx*dot.y() - vz*dot.x() > 0d) {
                        vx1 = -vz;
                        vz1 = vx;
                    } else {
                        vx1 = vz;
                        vz1 = -vx;
                    }
                } else {
                    if (random.nextBoolean()) {
                        vx1 = -vz;
                        vz1 = vx;
                    } else {
                        vx1 = vz;
                        vz1 = -vx;
                    }
                }
                r += (1d - r) * Math.min(d/0.69d, 1d);
            } catch (Exception e) {
                if (random.nextBoolean()) {
                    vx1 = -vz;
                    vz1 = vx;
                } else {
                    vx1 = vz;
                    vz1 = -vx;
                }
            }
            r *= 0.6d;
            double l = Math.sqrt(1d -r*r);
            victim.addVelocity((vx*l +vx1*r) *multiplier, 0.4d *random.nextDouble() *v *multiplier, (vz*l +vz1*r) *multiplier);
        }
        if (victim instanceof LivingEntity){
            float damage = CONFIG.getDamage(v*20d, m/STANDARD_MASS);
            if (damage > 0.01f) {
                victim.damage(train.getDamageSources().flyIntoWall(), damage);
            }
        }
        this.crashedEntities.add(victim);
    }

    private MinecartTrain(ServerWorld world, NbtList list) {
        this.world = world;
        this.units = new ArrayList<>();
        for (NbtElement element : list.stream().toList()){
            this.units.add(new MinecartUnit((NbtIntArray) element));
        }
        this.crashedEntities = new HashSet<>();
        this.tickRemains = 0d;
    }

    private MinecartTrain(ServerWorld world, List<MinecartUnit> units){
        this.world = world;
        this.units = units;
        this.units.replaceAll(unit -> this.new MinecartUnit(unit));
        this.crashedEntities = new HashSet<>();
        this.tickRemains = 0d;
    }

    private MinecartTrain(AbstractMinecartEntity entity){
        this.world = (ServerWorld) entity.getWorld();
        this.units = new ArrayList<>();
        this.units.add(new MinecartUnit(entity));
        this.crashedEntities = new HashSet<>();
        this.tickRemains = 0d;
    }

    public void tick(boolean hasGravity){
        if (this.tickRemains <= 0d){
            return;
        }
        MinecartTrain newTrain = null;
        for (int i = 0; i < this.units.size(); ++i){
            if (this.units.get(i).isLost()){
                newTrain = onUnitDestroyed(i);
                break;
            }
        }
        if (!this.notReady() && this.units.size() >1){
            if (hasGravity){
                this.units.forEach(unit -> {
                    double d = 2d*unit.entity.getFinalGravity();
                    unit.entity.setVelocity(unit.entity.getVelocity().add(0d, -d, 0d));
                });
            }
            boolean firstMove = true;
            boolean canMove = true;
            while (this.tickRemains > 1.0E-4 && !this.units.isEmpty()) {
                AbstractMinecartEntity front = this.units.getFirst().entity;
                double delta = front.railway_accelerator$moveOneStep(this.tickRemains, firstMove);
                if (this.units.getFirst().isLost()) {
                    this.units.removeFirst().release();
                    continue;
                }
                this.tickRemains -= delta;
                if (delta < 1.0E-4) {
                    continue;
                }
                if (front instanceof FurnaceBehavior furnace){
                    canMove = canMove && furnace.railway_accelerator$canMove();
                }
                double mass = front.railway_accelerator$getMass();
                double push = front.railway_accelerator$getPush();
                double resistance = front.railway_accelerator$getResistance();
                double lineV = front.railway_accelerator$getLineVelocity();
                for (int i = 1; i < this.units.size(); ++i) {
                    MinecartUnit unit = this.units.get(i);
                    MinecartPlus.ConnectionStatus status = unit.entity.railway_accelerator$follow(front, delta, firstMove);
                    if (!status.isConnect()) {
                        unit.entity.setVelocity(unit.entity.getVelocity().multiply(0.5d));
                        newTrain = splitAt(i, this.tickRemains);
                        lineV *= status.getVelocityMultiplier();
                        break;
                    }
                    if (unit.isLost()) {
                        newTrain = onUnitDestroyed(i);
                        break;
                    }
                    front = unit.entity;
                    if (front instanceof FurnaceBehavior furnace){
                        canMove = canMove && furnace.railway_accelerator$canMove();
                    }
                    mass += front.railway_accelerator$getMass();
                    push += front.railway_accelerator$getPush();
                    resistance += front.railway_accelerator$getResistance();
                }
                if (!canMove || (lineV < 1.0E-3 && MathTools.between(push, -1.0E-4, 1.0E-4))){
                    lineV = 0d;
                }else {
                    lineV += 0.2d *delta * (push - resistance)/mass;
                    if (lineV < 0d) {
                        if (push < 0d) {
                            lineV = -lineV;
                            flip();
                        } else {
                            lineV = 0d;
                        }
                    }
                }
                for (MinecartUnit unit : this.units){
                    unit.entity.railway_accelerator$updateLineVelocity(lineV);
                }
                firstMove = false;
            }
        }
        this.tickRemains = 0d;
        if (newTrain != null){
            newTrain.tick(false);
        }
    }

    public boolean isNeighbor(AbstractMinecartEntity minecart, Entity entity){
        for (int i = 0; i < this.units.size(); ++i){
            if (minecart.getUuid().equals(this.units.get(i).uuid)){
                boolean bl = false;
                MinecartUnit unit;
                try {
                    unit = this.units.get(i-1);
                    bl = entity.getUuid().equals(unit.uuid)
                            || entity.getUuid().equals(Objects.requireNonNull(unit.entity.getFirstPassenger()).getUuid());
                }catch (Exception ignored){}
                if (!bl){
                    try {
                        unit = this.units.get(i+1);
                        bl = entity.getUuid().equals(unit.uuid)
                                || entity.getUuid().equals(Objects.requireNonNull(unit.entity.getFirstPassenger()).getUuid());
                    }catch (Exception ignored){}
                }
                return bl;
            }
        }
        return false;
    }

    public void onCollision(AbstractMinecartEntity minecart){
        if (notReady()){
            return;
        }
        int index = 0;
        for (MinecartUnit unit : this.units){
            if (minecart.getUuid().equals(unit.uuid)){
                onUnitDestroyed(index);
                return;
            }
            ++index;
        }
    }

    public double getMass(){
        double m = 0d;
        for (MinecartUnit unit : this.units){
            if (unit.entity != null){
                m += unit.entity.railway_accelerator$getMass();
            }else {
                return 0d;
            }
        }
        return m;
    }

    public boolean isHead(AbstractMinecartEntity entity){
        return entity.getUuid().equals(this.units.getFirst().uuid);
    }

    public boolean isTail(AbstractMinecartEntity entity){
        return entity.getUuid().equals(this.units.getLast().uuid);
    }

    @Override
    public boolean equals(Object o){
        return o instanceof MinecartTrain train && !train.unitConflictWith(this);
    }

    private boolean unitConflictWith(MinecartTrain train){
        for (MinecartUnit unit : this.units){
            for (MinecartUnit unit1 : train.units){
                if (unit.equals(unit1)){
                    return true;
                }
            }
        }
        return false;
    }

    private void flip(){
        List<MinecartUnit> flipped = new ArrayList<>();
        for (int i = this.units.size() -1; i > -1; --i){
            MinecartUnit unit = this.units.get(i);
            unit.entity.railway_accelerator$flip();
            flipped.add(unit);
        }
        this.units = flipped;
    }

    private MinecartTrain splitAt(int index, double tickRemains){
        List<MinecartUnit> unitsToDrop = new ArrayList<>();
        while (index < this.units.size()){
            unitsToDrop.add(this.units.remove(index));
        }
        if (unitsToDrop.size() > 1) {
            MinecartTrain newTrain = new MinecartTrain(this.world, unitsToDrop);
            ALL_TRAINS.add(newTrain);
            newTrain.tickRemains = tickRemains;
            return newTrain;
        }else {
            unitsToDrop.forEach(MinecartUnit::release);
            return null;
        }
    }

    private boolean tryAccept(AbstractMinecartEntity entity){
        if (!entity.isRemoved()) {
            for (MinecartUnit unit : this.units) {
                if (unit.tryAccept(entity)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean startTick(){
        this.tickRemains = 1.0d;
        this.crashedEntities.clear();
        if (this.units.size() > 1) {
            for (MinecartUnit unit : this.units) {
                if (unit.isReady()) {
                    return false;
                }
            }
        }
        return true;
    }

    private boolean notReady(){
        for (MinecartUnit unit : this.units){
            if (!unit.isReady()){
                return true;
            }
        }
        return false;
    }

    private MinecartTrain onUnitDestroyed(int index){
        List<MinecartUnit> unitsToDrop = new ArrayList<>();
        this.units.remove(index).release();
        while (index < this.units.size()){
            unitsToDrop.add(this.units.remove(index));
        }
        if (unitsToDrop.size() > 1) {
            MinecartTrain newTrain = new MinecartTrain(this.world, unitsToDrop);
            ALL_TRAINS.add(newTrain);
            newTrain.tickRemains = this.tickRemains;
            return newTrain;
        }else {
            unitsToDrop.forEach(MinecartUnit::release);
            return null;
        }
    }

    private void remove(){
        ALL_TRAINS.remove(this);
        for (MinecartUnit unit : this.units){
            unit.release();
        }
        this.tickRemains = 0d;
    }

    private class MinecartUnit{
        AbstractMinecartEntity entity;
        final UUID uuid;

        private MinecartUnit(AbstractMinecartEntity entity){
            this.entity = entity;
            this.uuid = entity.getUuid();
            entity.railway_accelerator$setTrain(MinecartTrain.this);
        }

        private MinecartUnit(MinecartUnit unit){
            this.entity = unit.entity;
            this.uuid = unit.uuid;
            if (this.entity != null){
                this.entity.railway_accelerator$setTrain(MinecartTrain.this);
            }
        }

        private MinecartUnit(NbtIntArray nbtID){
            this.entity = null;
            this.uuid = NbtHelper.toUuid(nbtID);
        }

        @Override
        public boolean equals(Object o){
            return o instanceof MinecartUnit unit && unit.uuid.equals(this.uuid);
        }

        private NbtIntArray toNbt(){
            return NbtHelper.fromUuid(this.uuid);
        }

        private boolean tryAccept(AbstractMinecartEntity entity){
            if (entity.getUuid().equals(this.uuid)){
                this.entity = entity;
                entity.railway_accelerator$setTrain(MinecartTrain.this);
                return true;
            }else {
                return false;
            }
        }

        private boolean isLost(){
            if (this.entity == null){
                return false;
            }else {
                if (this.entity.isRemoved()){
                    switch (this.entity.getRemovalReason()){
                        case KILLED, DISCARDED, CHANGED_DIMENSION -> {
                            return true;
                        }
                        case null, default -> {
                            this.entity = null;
                            return false;
                        }
                    }
                }else {
                    this.entity.railway_accelerator$setTrain(MinecartTrain.this);
                    return false;
                }
            }
        }

        private boolean isReady(){
            return this.entity != null && !this.entity.isRemoved();
        }

        private void release(){
            if (this.entity != null){
                this.entity.railway_accelerator$setTrain(null);
                this.entity = null;
            }
        }
    }
}
